/*
 * Copyright 2007 Dan Hodge
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.datajanitor.baja.rest;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.xpath.XPathExpressionException;

import static net.datajanitor.baja.rest.xml.XMLUtils.buildDocument;
import static net.datajanitor.baja.rest.xml.XMLUtils.getText;
import static net.datajanitor.baja.rest.xml.XMLUtils.query;
import net.datajanitor.baja.data.BoxFileInfo;
import net.datajanitor.baja.data.UserInfo;
import net.datajanitor.baja.rest.xml.AttributeSupport;
import net.datajanitor.baja.rest.xml.GetTreeResponseHandler;
import net.datajanitor.baja.spi.BoxConnectionProvider;
import net.datajanitor.baja.spi.BoxSpiException;
import net.datajanitor.baja.spi.BuilderException;
import net.datajanitor.baja.spi.FileTreeBuilder;
import net.datajanitor.baja.spi.data.AuthInfo;
import net.datajanitor.baja.spi.data.FileUploadResult;
import net.datajanitor.baja.spi.support.UrlBuilder;

import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.ByteArrayPartSource;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.methods.multipart.FilePartSource;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.PartSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

class RestBoxConnectionProvider implements BoxConnectionProvider {
    private static final Log logger = LogFactory.getLog(RestBoxConnectionProvider.class);
    private final HttpClient client;
          
    private FileUploadResult doFileUpload(AuthInfo authInfo, BoxFileInfo parent, PartSource source) throws BoxSpiException {
        try {
            UrlBuilder uploadURLBuilder = new UrlBuilder().setHost("upload.box.net");
            uploadURLBuilder.setPath("api/1.0/upload/" + authInfo.getAuthToken() + "/" + parent.getId());
            URL uploadUrl = uploadURLBuilder.build();

            // TODO: send the correct content-type (and charset) so box.net can display files correctly in the browser
            PostMethod method = new PostMethod(uploadUrl.toString());
            MultipartRequestEntity multiPart = new MultipartRequestEntity(
                    new Part[] { new FilePart("new_file1", source, "application/octet-stream", "ISO-8859-1") },
                    method.getParams());  
            method.setRequestEntity(multiPart);
                        
            Document doc = build(this.client.executeMethod(method));
            String status = getStatus(doc);
            if ("upload_ok".equals(status)) {
                String query = "/response/files/file";            
                NodeList result = query(doc, query);
                
                // get all files even though only we're only doing one upload at a time - 
                // in the future, multi-file uploads will be supported
                List<FileUploadResult> results = new ArrayList<FileUploadResult>();
                for (int i = 0, len = result.getLength(); i < len; i++) {
                    AttributeSupport attrs = new AttributeSupport(result.item(0).getAttributes());
                    results.add(new FileUploadResult(attrs.getAsInt("id"), attrs.getAsInt("folder_id"), attrs.get("file_name")));
                }
                
                if (results.size() > 0) {
                    return results.get(0);
                } else {
                    throw new BoxSpiException("Error: No files were uploaded");
                }
            } else {
                throw new BoxSpiException("File upload failed, status = " + status);
            }
        } catch (HttpException e) {
            throw new BoxSpiException("Error connecting to HTTP server", e);
        } catch (BuilderException e) {
            throw new BoxSpiException("Error constructing request URL", e);
        } catch (IOException e) {
            throw new BoxSpiException("Error reading HTTP response", e);
        } catch (XPathExpressionException e) {
            throw new BoxSpiException("Error", e);
        }        
    }
    
    private InputStream doXMLGetAsStream(UrlBuilder builder) throws BoxSpiException {     
        try {
            URL requestUrl = builder.build();
            if (logger.isDebugEnabled()) {
                logger.debug("Issuing HTTP GET for URL: " + requestUrl);
            }

            HttpMethod method = new GetMethod(requestUrl.toString());
            return this.client.executeMethod(method);
        } catch (HttpException e) {
            throw new BoxSpiException("Error connecting to HTTP server", e);
        } catch (BuilderException e) {
            throw new BoxSpiException("Error constructing request URL", e);
        } catch (IOException e) {
            throw new BoxSpiException("Error reading HTTP response", e);
        }
    }
    
    private Document doXMLGetAsDocument(UrlBuilder urlBuilder) throws BoxSpiException {
        return build(doXMLGetAsStream(urlBuilder));
    }
  
    private String getStatus(Document doc) throws BoxSpiException {
        return getText(doc, "/response/status");
    }
    
    private Document build(InputStream input) throws BoxSpiException {
        try {
            return buildDocument(input);
        } catch (IOException e) {
            throw new BoxSpiException("Error reading HTTP response", e);
        } catch (ParserConfigurationException e) {
            throw new BoxSpiException("Error configuring parser", e);
        } catch (SAXException e) {
            throw new BoxSpiException("Error parsing response", e);
        }        
    }
            
    RestBoxConnectionProvider(HttpClient client) {
        this.client = client;
    }
            
    public RestBoxConnectionProvider() {
        this(new HttpClientImpl());
    }
    
    public AuthInfo authenticate(String ticket, String username, String password) throws BoxSpiException {
        try {
            UrlBuilder authURLBuilder = new UrlBuilder();
            authURLBuilder.setPath("api/1.0/auth/" + ticket);
            URL authUrl = authURLBuilder.build();

            PostMethod method = new PostMethod(authUrl.toString());
            method.addParameter("login", username);
            method.addParameter("password", password);
            method.addParameter("dologin", "1");
            method.addParameter("__login", "1");

            this.client.executeMethod(method);
            // assume success

            UrlBuilder authTokenURLBuilder = new RestUrlBuilder("get_auth_token");
            authTokenURLBuilder.addParam("ticket", ticket);

            Document doc = doXMLGetAsDocument(authTokenURLBuilder);
            String status = getStatus(doc);
            if ("get_auth_token_ok".equals(status)) {
                String authToken = getText(doc, "response/auth_token");
               
                NodeList nodes = query(doc, "/response/user/*/text()");
                UserInfo userInfo = new UserInfo(nodes);
            
                return new AuthInfo(userInfo, authToken);
            } else {
                throw new BoxSpiException("Authentication failed, status = " + status);
            }
        } catch (HttpException e) {
            throw new BoxSpiException("Error connecting to HTTP server", e);
        } catch (BuilderException e) {
            throw new BoxSpiException("Error constructing request URL", e);
        } catch (IOException e) {
            throw new BoxSpiException("Error reading HTTP response", e);
        } catch (XPathExpressionException e) {
            throw new BoxSpiException("Error running XPath query", e);
        }
    }

    public String createTicket() throws BoxSpiException {
        Document doc = doXMLGetAsDocument(new RestUrlBuilder("get_ticket"));

        String status = getStatus(doc);
        if ("get_ticket_ok".equals(status)) {
            return getText(doc, "response/ticket");
        } else {
            throw new BoxSpiException("get_ticket status = " + status);
        }
    }
    
    public void getTree(AuthInfo authInfo, final FileTreeBuilder fileTreeBuilder) throws BoxSpiException {
        UrlBuilder builder = new RestUrlBuilder("get_account_tree");
        builder.addParam("folder_id", "0");
        builder.addParam("params%5B%5D", "nozip");
        authInfo.bindParameter(builder, "auth_token");
        
        InputStream ins = doXMLGetAsStream(builder);
        try {
            SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
            parser.parse(ins, new GetTreeResponseHandler(fileTreeBuilder));
        } catch (ParserConfigurationException e) {
            throw new BoxSpiException("Error parsing the response", e);
        } catch (SAXException e) {
            throw new BoxSpiException("Invalid response", e);
        } catch (IOException e) {
            throw new BoxSpiException("Error parsing the response", e);
        } finally {
            if (ins != null) {
                try {
                    ins.close();
                } catch (IOException ignore) {
                }
            }
        }
    }

    public FileUploadResult uploadFile(AuthInfo authInfo, BoxFileInfo parent, File file) throws BoxSpiException {
        try {
            PartSource source = new FilePartSource(file);
            return doFileUpload(authInfo, parent, source);
        } catch (FileNotFoundException e) {
            throw new BoxSpiException("Error loading file: " + file, e);
        }
    }
        
    public FileUploadResult uploadFile(AuthInfo authInfo, BoxFileInfo parent, byte[] fileData, String fileName) throws BoxSpiException {
        PartSource source = new ByteArrayPartSource(fileName, fileData);
        return doFileUpload(authInfo, parent, source);
    }

    public InputStream streamFile(AuthInfo authInfo, BoxFileInfo file) throws BoxSpiException {
        try {
            UrlBuilder downloadURLBuilder = new UrlBuilder();
            downloadURLBuilder.setPath("api/1.0/download/" + authInfo.getAuthToken() + "/" + file.getId());
            URL downloadUrl = downloadURLBuilder.build();
            
            HttpMethod method = new GetMethod(downloadUrl.toString());
            return this.client.executeMethod(method);
        } catch (HttpException e) {
            throw new BoxSpiException("Error connecting to HTTP server", e);
        } catch (BuilderException e) {
            throw new BoxSpiException("Error constructing request URL", e);
        } catch (IOException e) {
            throw new BoxSpiException("Error reading HTTP response", e);
        }        
    }
        
    public boolean delete(AuthInfo authInfo, BoxFileInfo file) throws BoxSpiException {
        UrlBuilder deleteUrlBuilder = new RestUrlBuilder("delete");
        deleteUrlBuilder.addParam("target", file.isFile() ? "file" : "folder");
        deleteUrlBuilder.addParam("target_id", file.getId());
        authInfo.bindParameter(deleteUrlBuilder, "auth_token");

        Document doc = doXMLGetAsDocument(deleteUrlBuilder);
        return "s_delete_node".equals(getStatus(doc));
    }
}